package jeconbond.automata;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import jeconbond.economic.resources.IResourceType;
import jeconbond.economic.resources.ResourceBasket;
import jeconbond.economic.resources.ResourceStorrage;
import jeconbond.experiment.BaseJEBExperimentSettings;
import jeconbond.experiment.process.BaseJEBExperimentContext;
import jeconbond.experiment.process.IJEBExperimentContext;

import laboratoryQ.BaseUtils;
import laboratoryQ.computation.IRandom;

public class AutomataUtils {
	private static abstract class BasePathDFSContext {
		INode root;
		Stack<IOutcome> track = new Stack<IOutcome>();
		
		public BasePathDFSContext(
				INode root
		) {
			this.root = root;
		}
		
		public void doDFS() {
			dfs(root);
		}
		
		private void dfs(INode n) {
			for (IOutcome out : n.getOutcomes()) {
				track.add(out);
				if (out.getDestination() == root) {
					onTrackFound();
				} else {
					dfs(out.getDestination());
				}
				track.pop();
			}
		}

		protected abstract void onTrackFound();
	}
	
	private static class DFSCostsContext extends BasePathDFSContext {
		ResourceStorrage costs;
		ResourceStorrage profit;
		
		public DFSCostsContext(
				INode root, 
				ResourceStorrage costs,
				ResourceStorrage profit
		) {
			super(root);
			this.costs = costs;
			this.profit = profit;
		}
		
		@Override
		protected void onTrackFound() {
			for (IOutcome out : track) {
				for (ResourceBasket rb : out.getRequirements()) {
					costs.storeResource(rb);
				}
				for (ResourceBasket rb : out.getProducedResources()) {
					profit.storeResource(rb);
				}
			}
		}
	}
	
	public static void dfsCosts(
			INode root, 
			ResourceStorrage costs,
			ResourceStorrage profit
	) {
		new DFSCostsContext(
				root, 
				costs,
				profit
		).doDFS();
	}
	
	private static class DFSMultiplyProfitContext {
		private INode root;
		private Map<IResourceType, Double> mults;
		private Set<INode> visited = new HashSet<INode>();

		public DFSMultiplyProfitContext(
				INode root,
				Map<IResourceType, Double> mults
		) {
			this.root = root;
			this.mults = mults;
		}

		private void dfs(INode node) {
			visited.add(node);
			for (IOutcome out : node.getOutcomes()) {
				for (ResourceBasket rb : out.getProducedResources()) {
					Double mul = mults.get(rb.type);
					if (mul != null) {
						rb.value *= mul;
					}
				}
				INode dest = out.getDestination();
				if (!visited.contains(dest)) {
					dfs(dest);
				}
			}
		}
		
		public void doDFS() {
			dfs(root);
		}
	}

	public static void dfsMultiplyProfit(
			INode root,
			Map<IResourceType, Double> mults
	) {
		new DFSMultiplyProfitContext(root, mults).doDFS();
	}
	
	private static class DFSGetAllOutcomesContext {
		private INode root;
		private Set<INode> visited = new HashSet<INode>();
		private List<IOutcome> outcomes = new ArrayList<IOutcome>();

		public DFSGetAllOutcomesContext(INode root) {
			this.root = root;
		}

		public void doDFS() {
			dfs(root);
		}

		private void dfs(INode node) {
			visited.add(node);
			for (IOutcome out : node.getOutcomes()) {
				outcomes.add(out);
				INode dest = out.getDestination();
				if (!visited.contains(dest)) {
					dfs(dest);
				}
			}
		}
	}

	public static List<IOutcome> dfsGetAllOutcomes(INode root) {
		DFSGetAllOutcomesContext context = new DFSGetAllOutcomesContext(root);
		context.doDFS();
		return context.outcomes;
	}
	
	public static Set<INode> dfsGetAllNodesSet(INode root) {
		List<IOutcome> outs = dfsGetAllOutcomes(root);
		Set<INode> nodes = new HashSet<INode>();
		for (IOutcome out : outs) {
			nodes.add(out.getDestination());
		}
		if (!nodes.contains(root)) {
			nodes.add(root);
		}
		return nodes;
	}
	
	public static List<INode> dfsGetAllNodesList(INode root) {
		List<INode> nodes = new ArrayList<INode>();
		Set<INode> nodesSet = dfsGetAllNodesSet(root);
		nodes.addAll(nodesSet);
		return nodes;
	}
	
	public static void easyTracePlan(INode root, INode start, List<IOutcome> plan) {
		INode cur = start;
		while (true) {
			IOutcome out = cur.getOutcomes().get(0);
			plan.add(out);
			cur = out.getDestination();
			if (cur == root) {
				break;
			}
		}
	}

	public static void easyTracePlan(INode root, List<IOutcome> plan) {
		easyTracePlan(root, root, plan);
	}

	public static List<IOutcome> easyTracePlan(INode root) {
		List<IOutcome> plan = new ArrayList<IOutcome>();
		easyTracePlan(root, plan);
		return plan;
	}
	
	public static void filterOutcomesWithProfit(List<IOutcome> list) {
		for (Iterator<IOutcome> it = list.iterator(); it.hasNext();) {
			IOutcome out = it.next();
			if (out.getProducedResources().length == 0) {
				it.remove();
			}
		}
	}
	
	public interface IProfitCostGenManager {
		public double addRandomCost(INode[] automatas, IResourceType resource);
		public double addRandomProfit(INode[] automatas, IResourceType resource);
		public ResourceBasket[] generateCosts();
		public ResourceBasket[] generateProfit();
		/**
		 * Call this method after branch generation completed.
		 * Design note: not the best but the easiest solution 2 generate complementary || substitute relations.
		 */
		public void onBranchCompleted();
	}

	public interface IProfCostManagerFactory {
		public IProfitCostGenManager newManager(IJEBExperimentContext iexperimentContext);
	}

	public static void makeProfitable(
			INode[] automatas, 
			IResourceType[] resources, 
			double produceProfit, 
			IProfitCostGenManager profitCostManager 
	) {
		ResourceStorrage costs = new ResourceStorrage();
		ResourceStorrage profits = new ResourceStorrage();
		for (INode root : automatas) {
			AutomataUtils.dfsCosts(root, costs, profits);
		}
		Map<IResourceType, Double> mults = new HashMap<IResourceType, Double>();
		for (IResourceType resource : resources) {
			double cost = costs.howMuchWasStored(resource);
			double profit = profits.howMuchWasStored(resource);
			if (profit == 0.0) {
				profit = profitCostManager.addRandomProfit(automatas, resource);
			}
			if (cost == 0.0) {
				cost = profitCostManager.addRandomCost(automatas, resource); 
			}
			if (cost == 0.0 || profit == 0.0) throw new RuntimeException("Contract failue.");
			double profitMul = (cost / profit) * produceProfit;
			mults.put(resource, profitMul);
		}
		for (INode node : automatas) {
			AutomataUtils.dfsMultiplyProfit(node, mults);
		}
	}
	
	public static ResourceBasket[] defaultGenerateRandomBasket(BaseJEBExperimentContext experimentContext) {
		BaseJEBExperimentSettings settings = experimentContext.getExperimentSettings();
		IResourceType[] resources = settings.resources;
		IRandom random = experimentContext.getRandom();
		ResourceBasket rb = new ResourceBasket(
				resources[BaseUtils.randomInt(random, resources.length)],
				random.get()
		);
		return new ResourceBasket[] { rb };
	}

	public static INode generateChain(
			int chainLength,
			String prefix, 
			IProfitCostGenManager profitCostGenManager
	) {
		INode root = new Node(prefix + "root");
		generateChain(
				root,
				chainLength,
				prefix, 
				profitCostGenManager
		);
		return root;
	}
	
	public static void generateChain(
			INode root,
			int chainLength,
			String prefix,
			IProfitCostGenManager profitCostGenManager
	) {
		generateChain(
				root,
				root,
				chainLength,
				prefix,
				profitCostGenManager
		);
	}

	public static void generateChain(
			INode beg, 
			INode end, 
			int chainLength, 
			String prefix, 
			IProfitCostGenManager profitCostGenManager
	) {
		INode prev = beg;
		for (int i = 1; i < chainLength; i++) {
			INode dest = new Node(prefix + i);
			IOutcome out = new Outcome(
					dest,
					profitCostGenManager.generateCosts()
			);
			prev.getOutcomes().add(out);
			prev = dest;
		}
		IOutcome prodOut = new Outcome(end);
		prodOut.setProducedResources(profitCostGenManager.generateProfit());
		prev.getOutcomes().add(prodOut);
	}

	public static int getLengthOfEasyTrace(INode beg, INode end) {
		int result = 0;
		while (beg != end) {
			beg = beg.getOutcomes().get(0).getDestination();
			result++;
		}
		return result;
	}

	private static class DFSRoot2RootPathContext extends BasePathDFSContext {
		List<List<IOutcome>> result = new ArrayList<List<IOutcome>>();
		
		public DFSRoot2RootPathContext(INode root) {
			super(root);
		}

		protected void onTrackFound() {
			List<IOutcome> curTrack = new ArrayList<IOutcome>();
			curTrack.addAll(track);
			result.add(curTrack);
		}
	}
	
	public static List<List<IOutcome>> dfsRoot2RootPath(INode root) {
		DFSRoot2RootPathContext cont = new DFSRoot2RootPathContext(root);
		cont.doDFS();
		return cont.result;
	}
	
	private static class DFSPerspectiveForksCount {
		private static final Integer INTEGER_MIN_ONE = -1;
		private INode root;
		private Map<INode, Integer> result;
		public boolean backCall = false;
		public DFSPerspectiveForksCount(INode root, Map<INode, Integer> result) {
			super();
			this.root = root;
			this.result = result;
		}
		public void doDFS() {
			dfs(root);
		}
		private int dfs(INode cur) {
			if (cur == root && result.size() > 0) {
				return 1;
			}
			if (result.containsKey(cur)) {
				backCall = true;
				return -(1 << 25); 
			}
			int res = 0;
			result.put(cur, INTEGER_MIN_ONE);
			for (IOutcome out : cur.getOutcomes()) {
				res += dfs(out.getDestination()) - 1;
			}
			res += cur.getOutcomes().size();
			result.put(cur, res);
			return res;
		}
	}

	/**
	 * measure of branching.
	 */
	public static Map<INode, Integer> dfsPerspectiveForksCount(INode root) {
		Map<INode, Integer> result = new HashMap<INode, Integer>();
		DFSPerspectiveForksCount dfsPerspectiveForksCount = new DFSPerspectiveForksCount(root, result);
		dfsPerspectiveForksCount.doDFS();
		if (dfsPerspectiveForksCount.backCall) { // this algorithm can't compute the task.
			return null;
		}
		return result;
	}
}
